test(scripts): derive every handler key a registered renderer reads, and require its arm to declare it (objectui#7753) - #7805
Merged
Conversation
…er reads and requires its arm to declare it (objectui#7753)
`BaseSchema` is `.passthrough()`, so an `on*` key that leaves an arm is not
refused — it stops being judged and the value is KEPT. objectui#7664 measured
that on the built dist: `{ type: 'kanban', columns: [], onCardClick: { action:
'toast' } }` went from REFUSED to ACCEPTED with the object surviving into the
parsed output, while every gate stayed green. The objectui#6124 ledger could not
see it — its population is two hand-written arrays, and the change re-keyed the
arm by SUBSTITUTION, holding `RUNTIME_SLOT` at 44 and `ALL_SITES` at 66.
This adds the derivation that was missing, generalising suite 3 of
`plugin-kanban/src/__tests__/kanban-handler-slots-7664.test.tsx` from one arm to
the whole registry: arms from every `type: z.literal(…)` in
`packages/types/src/zod`, read sites from every real `ComponentRegistry.register`
call and the components each registration hands the SAME document to. Both
populations are derived; the ledger here is an exemption list only.
Verified red on the real pre-remediation tree (bd1fc71), naming
`'kanban'.onCardClick`, with one finding more than `main` and no other
difference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #7753
Option 1 of the card, per the PM ruling on it: one census that walks every registered renderer for
schema.onX/props.onXreads and requires each to be a declared member of its arm. It lives inscripts/because the read-site half is spread across@object-ui/plugin-*andpackages/components, which@object-ui/typesmay not import. Shape copied from PR #7789: acheck-*.mjsexporting its internals, ascripts/__tests__/pin driving them over mkdtemp fixture trees, one step inci.yml'stype-checkjob, onepackage.jsonscript, and the matching row incontent/docs/guide/ci-cd-pipeline.md(obliged by #3653, which pins that table by command).Both populations are DERIVED. Arms come from every
type: z.literal(...)object literal inpackages/types/src/zod, with members unioned along.extend()bases and resolved object spreads. Read sites come from every realComponentRegistry.register('TYPE', C, ...)CALL, taking the handler reads inCand in every componentChands THE SAME DOCUMENT to. The only hand-written list isKNOWN_UNDECLARED_READS, which is an exemption list, never the population: every row names a card and a row whose read site the gate can no longer find FAILS it.All numbers below were taken at
973255681, the head of this branch.Reading 1 — it fires on real history, on the unmutated tree
bd1fc71118a6e9744f4ace14110ac49a93a9be63is the pre-remediation head of PR #7743 (the treekanban-handler-slots-7664.test.tsx's own docblock names). It is a real commit in this repository, checked out into a detached worktree and not modified in any way. At that tree the'kanban'arm declaresonCardMove,onQuickAdd,onColumnAdd,onCardAddand NOTonCardClick, whileKanbanRendererstill forwardsschema.onCardClickatindex.tsx:194.40 findings there against 39 on
main, and the one extra is exactly'kanban'.onCardClick. This gate would have reddened CI on the day of the incident. No reconstruction was needed.Reading 2 — green on
main, with its own census counts106 arms derived from the mirrors, 3071 arm members in total, 216 registrations walked, 120 of them at a type that has an arm, 62 reachable handler reads judged across 25 types. The green is never "the walk found nothing": the CLI exits 1 if the census falls under its floors, and the pin asserts non-zero counters on the repository run and on every fixture that passes.
Reading 3 — a control, and a control on the control
Both legs planted on the committed tree, restored under
trap ... EXIT INT TERM, each mutation proved on disk by blob hash BEFORE the gate was read and each restoration proved by blob-hash equality plus an emptygit diff HEAD.HEADblob forpackages/types/src/zod/complex.zod.tsis4bb6bcf1b81453730f52a068667e3ca53417b1b7.Leg A — really delete a still-read key (87-byte member line
onCardClick: handlerKeyRefusal(...)removed from the'kanban'arm; anchor occurrences 1 to 0):Leg B — the same operation, on a member nothing reads (96-byte member line
coverImageField: z.string()...removed from the SAME arm in the SAME file; anchor occurrences 1 to 0):Identical edit shape, comparable size, opposite verdicts. The red is caused by semantics — a still-read key leaving an arm — not by the file having been edited.
A third leg is pinned rather than run by hand: a still-read key declared with the RETIRED disposition instead of deleted goes red as
retired-but-read.False positives on
main: zero, and the zero is comparativeThe first, deliberately coarse cut — "any
schema.onX/props.onXread anywhere in the FILE of a registration must be a declared arm member" — produced 36 findings. Every narrowing below is named after the class it removed, and each is pinned in the test file:packages/types/src/complex.tsNAMESComponentRegistry.register(...)in doc comments eleven times and registers nothing, so its own interfaces read as handler reads on three chatbot armsX.pick({...}).shape)chatbot-enhanced/chatbot-floatingonErrorandonSendARE declared, through...ChatbotSharedMirrorShapeprops.onXscoped to the component's own outermost parameters'menubar'.onClick, fromitems.map((child) => child.onClick?.()), which is a menu ITEM's handlerplugin-kanban/index.tsxandplugin-chatbot/renderer.tsxTwo more false-positive classes appeared only after the walk was made transitive (which it had to be — see below), and both are pinned:
'dashboard'.onRowClickand'dashboard'.onClick— a dashboard's widgets each get their own document, soObjectDataTable'sschema.onRowClickis a read of the widget's document. Removed by following a JSX child only when the parent's document flows into itsschema=attribute.'object-view'.onViewChange—ObjectViewbuilds{ type: 'view-switcher', ..., storageKey: \view-pref-${schema.objectName}` }and hands it to. It mentionsschema, so a mention test called it the parent's document and reportedViewSwitcher's read against an arm that is not even the one being read. Removed by treating an object literal that writes its owntype` member as a new document.One false NEGATIVE is worth recording too, because it is the failure this gate could most easily have shipped with: the first cut started the walk at the register call's second argument, which is usually an identifier. It found ten read sites, was green on
main, and was green onbd1fc7111— it never reachedKanbanRendererat all.'kanban'registersObjectKanbanRenderer, which is an HOC, which rendersObjectKanbanthrough a render-prop parameter, which rendersKanbanRendererthrough an object spread. All four hops are load-bearing and all four are pinned as a fixture chain.Coverage: 20 of the 67 ledger sites, and which 47 are not covered
The #6124 ledger is
RUNTIME_SLOT(45) +RETIRED(22) = 67 sites. The existingkanban-handler-slots-7664.test.tsxcovers 3.RUNTIME_SLOTsites are covered:KanbanSchema.onCardMove/.onCardClick/.onQuickAdd,ChatbotSchema.onError/.onSend,ChatbotEnhancedSchema.onError/.onSend/.onClear,ChatbotFloatingSchema.onError/.onSend/.onClear,DataTableSchema.onRowEdit/.onRowDelete/.onSelectionChange/.onColumnsReorder,FormSchema.onCancel,CodeEditorSchema.onChange,CardSchema.onClick,ObjectDataTableSchema.onRowClick,AlertDialogSchema.onAction.RETIREDsites, and this is a design boundary rather than a defect: a tombstone exists precisely because nothing reads the key, so it has no read site to derive from, by construction. What this gate adds there is the other direction — a renderer that ever starts reading a tombstoned key is aretired-but-readfinding.RUNTIME_SLOTsites are not covered, and the reason is one channel: they reach the renderer without the key ever being NAMED asschema.onXorprops.onX. Either the props are spread onto a Radix root or DOM listener slot (AccordionSchema.onValueChange,CollapsibleSchema.onOpenChange,ToggleGroupSchema.onValueChange,TabsSchema.onValueChange,ButtonSchema.onClick,MenuItemSchema.onClick, and the sevenonOpenChangeoverlaysDialog/AlertDialog/Sheet/Drawer/Popover/HoverCard/DropdownMenu), or the handler is destructured straight out of props (InputSchema.onChange,TextareaSchema.onChange,SelectSchema.onChange,CheckboxSchema.onChange,FileUploadSchema.onChange,DatePickerSchema.onChange,InputOTPSchema.onChange,PaginationSchema.onPageChange,FilterBuilderSchema.onChange,CalendarViewSchema.onViewChange,FormSchema.onSubmit,FormSchema.onChange).The destructured half was measured, not assumed. Extending the census to
({ schema, onChange, ... })raisesRUNTIME_SLOTcoverage from 20 to 28 of 45 — and adds 86 findings (125 total) at full transitivity, or 11 (50 total) when limited to the registered component. Those 11 includeview-switcher::ViewSwitcherSchema.onCreateViewand.onViewAction, whichObjectViewpasses as ordinary React props: a destructuredonXcannot be told apart statically from a host-supplied callback, so the extension buys coverage with rows nobody could act on. It is left out, and this paragraph is the record of the trade rather than a silent omission.The gate's own first findings: 39, ledgered, not fixed here
The census also judges read sites the #6124 ledger never contained — 42 of the 62, of which 39 are keys a registered renderer reads that its arm never declared at all. That is the same passthrough exposure the card names, standing on
maintoday (AlertDialogSchema.onActionwas exactly this shape until #7104 declared it). They are filed as #7804 and carried inKNOWN_UNDECLARED_READS, every row naming that card, because each fix is apackages/PKG/srcchange and this card is dispatched Clause-②no— and because each key's #6124 disposition has to be measured, not guessed. The ledger drains row by row; a row whose read site disappears fails the gate.Scope
No file under
packages/is touched —git diff --name-only origin/main..HEAD | grep -c '^packages/'is0.handler-keys-json-refusal-6124.test.ts, every*.zod.tsandkanban-handler-slots-7664.test.tsxare unmodified. The gate observes; it changes no schema and no accept/refuse behaviour.Verification run
node scripts/check-changeset-presence.mjs— exit 0, verbatim:✅ No source or published contract of a released package changed in this range, so no changeset is owed.(5 files counted). No changeset added.pnpm type-check:scripts(tsc -p tsconfig.scripts.json) — exit 0.pnpm exec vitest run scripts/__tests__— exit 0, 104 files, 3056 tests passed, at973255681. That union includes this gate's new pin (21 tests),ci-cd-pipeline-doc.test.ts(the ci-cd-pipeline.md 的 type-check 行漏了pnpm check:i18n-keys,而 ci-cd-pipeline-doc.test.ts 的钉子按「作业」而非「步骤」判定,看不见这类漏项 #3653 by-command pairing),lint-workflow.test.ts,merge-queue-reporting.test.tsandscripts-type-check.test.ts.node scripts/check-control-bytes.mjs— exit 0, 6358 tracked text files scanned.🤖 Generated with Claude Code
https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
Generated by Claude Code